<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>科学馆 VR 展厅 · Three.js</title>
  <!-- 主题蓝色与写实风格的基础样式 -->
  <style>
    html, body {
      margin: 0;
      height: 100%;
      overflow: hidden;
      background: #0a1a2a; /* 深蓝基调 */
      color: #cfe9ff;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', sans-serif;
    }
    #app { height: 100%; }
    canvas { display: block; }

    /* 右上角操作提示与 UI */
    .hud {
      position: fixed;
      top: 12px;
      right: 12px;
      background: rgba(6, 24, 48, 0.6);
      border: 1px solid rgba(96, 169, 255, 0.35);
      box-shadow: 0 6px 20px rgba(0,0,0,0.35), inset 0 0 24px rgba(64,140,255,0.15);
      border-radius: 10px;
      padding: 12px 14px;
      line-height: 1.5;
      backdrop-filter: blur(6px);
      user-select: none;
    }
    .hud h3 { margin: 0 0 6px; font-size: 14px; color: #9fd0ff; }
    .hud kbd {
      background: #0d2a4a;
      border: 1px solid #2d6cdf;
      border-bottom-width: 2px;
      border-radius: 6px;
      padding: 1px 6px;
      margin: 0 2px;
      color: #d7ecff;
      font-size: 12px;
      box-shadow: inset 0 0 10px rgba(77,158,255,0.25);
    }

    /* 屏幕中央的锁定提示 */
    #lockHint {
      position: fixed;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      background: radial-gradient(ellipse at center, rgba(10,26,42,0.6) 0%, rgba(10,26,42,0.85) 55%, rgba(5,14,26,0.95) 100%);
      color: #cfe9ff;
      z-index: 5;
      cursor: pointer;
    }
    #lockHint .panel {
      padding: 22px 26px;
      border: 1px solid rgba(120,188,255,0.4);
      border-radius: 12px;
      background: linear-gradient(180deg, rgba(12,36,66,0.8), rgba(8,22,40,0.9));
      box-shadow: 0 10px 40px rgba(0,0,0,0.45), inset 0 0 30px rgba(90,168,255,0.15);
      text-align: center;
    }
    #lockHint h1 { margin: 0 0 10px; font-size: 20px; color: #a8d6ff; }
    #lockHint p { margin: 6px 0; font-size: 14px; opacity: .9; }

    /* 底部蓝色科技感地坪 UI 辅助 (非必须，仅做风格化) */
    .floorGlow {
      position: fixed;
      bottom: -20vh;
      left: -10vw;
      width: 140vw;
      height: 50vh;
      pointer-events: none;
      background: radial-gradient(ellipse at center, rgba(80,160,255,0.18) 0%, rgba(20,80,200,0.06) 45%, rgba(0,0,0,0) 70%);
      filter: blur(18px);
      z-index: 0;
    }

    /* 交互检查提示（靠近展台时） */
    #interactTip {
      position: fixed;
      left: 50%;
      bottom: 8%;
      transform: translateX(-50%);
      background: rgba(8,28,50,0.7);
      border: 1px solid rgba(96,169,255,0.35);
      color: #d7ecff;
      padding: 8px 12px;
      border-radius: 8px;
      font-size: 13px;
      display: none;
      z-index: 6;
    }

    /* 顶部标题条，参考外立面科技馆感 */
    .topbar {
      position: fixed; left: 20px; top: 16px; z-index: 6;
      padding: 8px 12px; border-radius: 8px;
      background: rgba(7, 28, 52, 0.6);
      border: 1px solid rgba(120,188,255,0.35);
      box-shadow: 0 6px 18px rgba(0,0,0,.35), inset 0 0 20px rgba(77,158,255,.18);
    }
    .topbar b { color: #9fd0ff; }
    .topbar small { color: #77b5ff; }
  </style>
</head>
<body>
  <script>
    // 如果不在 React 版路由，重定向到 /mode
    (function(){ try { if (!location.pathname.match(/\/mode$/)) location.replace('/mode'); } catch(e){} })();
  </script>
  <noscript>此页面已迁移，请访问 /mode</noscript>
  <div id="app"></div>
  <div class="topbar"><b>科学馆 VR 展厅</b> <small>W A S D 移动，鼠标控制视角</small></div>

  <div class="hud">
    <h3>操作帮助</h3>
    <div>
      进入/退出视角锁定：<kbd>点击屏幕</kbd> 或 <kbd>Esc</kbd>
    </div>
    <div>
      移动：<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd>
      跳跃：<kbd>Space</kbd>
    </div>
    <div>
      查看模型：靠近展台出现提示后按 <kbd>E</kbd> 进入模型检查模式，鼠标拖动旋转；再次 <kbd>E</kbd> 退出。
    </div>
  </div>

  <div id="interactTip">按 E 进入模型检查模式</div>
  <div id="lockHint"><div class="panel">
    <h1>点击进入展厅</h1>
    <p>WASD 自由移动 · 鼠标视角</p>
    <p>靠近展台按 E 交互查看 3D 模型</p>
  </div></div>
  <div class="floorGlow"></div>

  <!-- Three.js 与扩展加载器（固定版本） -->
  <script src="https://unpkg.com/three@0.152.2/build/three.min.js"></script>
  <script src="https://unpkg.com/three@0.152.2/examples/js/controls/PointerLockControls.js"></script>
  <script src="https://unpkg.com/three@0.152.2/examples/js/controls/OrbitControls.js"></script>
  <script src="https://unpkg.com/three@0.152.2/examples/js/loaders/GLTFLoader.js"></script>

  <script>
    // ============================
    // 基础场景/渲染器/相机
    // ============================
    const app = document.getElementById('app');
    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.outputEncoding = THREE.sRGBEncoding;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    app.appendChild(renderer.domElement);
    // 让画布可聚焦，提升指针锁定成功率
    renderer.domElement.tabIndex = 0;
    setTimeout(()=>renderer.domElement.focus(), 0);

    const scene = new THREE.Scene();
    scene.fog = new THREE.Fog(0x0a1a2a, 60, 180);

    // 以天空穹顶和环境光营造写实蓝色调
    const skyGeo = new THREE.SphereGeometry(500, 32, 16);
    const skyMat = new THREE.ShaderMaterial({
      side: THREE.BackSide,
      transparent: false,
      uniforms: {
        topColor: { value: new THREE.Color(0x0a1a2a) },
        bottomColor: { value: new THREE.Color(0x0e2b4e) },
        offset: { value: 20 },
        exponent: { value: 0.6 }
      },
      vertexShader: `
        varying vec3 vWorldPosition;
        void main() {
          vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
          vWorldPosition = worldPosition.xyz;
          gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
      `,
      fragmentShader: `
        uniform vec3 topColor;
        uniform vec3 bottomColor;
        uniform float offset;
        uniform float exponent;
        varying vec3 vWorldPosition;
        void main() {
          // 以世界坐标的 y 高度作渐变，加入 offset 只影响 y 分量
          float h = normalize( vWorldPosition + vec3(0.0, offset, 0.0) ).y;
          gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h, 0.0 ), exponent ), 0.0 ) ), 1.0 );
        }
      `
    });
    const sky = new THREE.Mesh(skyGeo, skyMat);
    scene.add(sky);

    const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 500);
    camera.position.set(0, 1.7, 12); // 站在广场处

    // 主光源：冷蓝色顶灯 + 柔和环境光
    const hemi = new THREE.HemisphereLight(0x7fbfff, 0x0a1020, 0.6);
    scene.add(hemi);
    const dirLight = new THREE.DirectionalLight(0xb7d7ff, 1.0);
    dirLight.position.set(20, 40, 10);
    dirLight.castShadow = true;
    dirLight.shadow.mapSize.set(2048, 2048);
    scene.add(dirLight);

    // ============================
    // 地面与外立面入口（参考图一）
    // ============================
    const groundGeo = new THREE.PlaneGeometry(400, 400);
    const groundMat = new THREE.MeshStandardMaterial({ color: 0x0f2a48, roughness: .95, metalness: .05 });
    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    scene.add(ground);

    // 外立面体块（简化抽象，蓝绿色玻璃感）
    const facade = new THREE.Mesh(
      new THREE.BoxGeometry(26, 12, 20),
      new THREE.MeshPhysicalMaterial({ color: 0x1b3358, metalness: .6, roughness: .35, clearcoat: .6, transmission: 0, envMapIntensity: 0.8 })
    );
    facade.position.set(0, 6, -12);
    facade.castShadow = true; facade.receiveShadow = true;
    scene.add(facade);

    // 入口门洞
    const gate = new THREE.Mesh(
      new THREE.BoxGeometry(8, 6, 2),
      new THREE.MeshStandardMaterial({ color: 0x153a6b, roughness: 0.6, metalness: 0.2 })
    );
    gate.position.set(0, 3, -2);
    facade.add(gate);

    // 发光导视牌
    function makeSign(x, z, text = 'Exhibition'){
      const p = new THREE.Mesh(
        new THREE.BoxGeometry(0.4, 3, 0.8),
        new THREE.MeshStandardMaterial({ color: 0x1e5db8, emissive: 0x184c96, emissiveIntensity: .8, metalness: .4, roughness: .2 })
      );
      p.position.set(x, 1.5, z);
      p.castShadow = true; p.receiveShadow = true;
      const plate = new THREE.Mesh(
        new THREE.PlaneGeometry(0.7, 1.2),
        new THREE.MeshBasicMaterial({ color: 0x99ccff })
      );
      plate.position.set(0, 1.1, 0.41);
      p.add(plate);
      return p;
    }
    scene.add(makeSign(-4, 6, 'Hall A'));
    scene.add(makeSign( 4, 6, 'Hall B'));

    // ============================
    // 室内展厅与三个房间
    // ============================
    const rooms = new THREE.Group();
    rooms.position.set(0, 0, -26); // 在外立面后方
    scene.add(rooms);

    function makeRoom(width, height, depth, color) {
      const g = new THREE.Group();
      const mat = new THREE.MeshStandardMaterial({ color, metalness: .1, roughness: .85 });
      // 地板
      const floor = new THREE.Mesh(new THREE.PlaneGeometry(width, depth), mat);
      floor.rotation.x = -Math.PI/2; floor.receiveShadow = true; g.add(floor);
      // 墙体（四周）
      const wallMat = new THREE.MeshStandardMaterial({ color: color + 0x07111a, metalness: .2, roughness: .7 });
      const wallGeo1 = new THREE.PlaneGeometry(width, height);
      const wallGeo2 = new THREE.PlaneGeometry(depth, height);
      const front = new THREE.Mesh(wallGeo1, wallMat); front.position.set(0, height/2, -depth/2); g.add(front);
      const back  = new THREE.Mesh(wallGeo1, wallMat); back.position.set(0, height/2,  depth/2); back.rotation.y = Math.PI; g.add(back);
      const left  = new THREE.Mesh(wallGeo2, wallMat); left.position.set(-width/2, height/2, 0); left.rotation.y = Math.PI/2; g.add(left);
      const right = new THREE.Mesh(wallGeo2, wallMat); right.position.set( width/2, height/2, 0); right.rotation.y = -Math.PI/2; g.add(right);
      // 顶部灯带
      const ceil = new THREE.Mesh(new THREE.PlaneGeometry(width, depth), new THREE.MeshStandardMaterial({ color: color + 0x111a2a, emissive: 0x184c96, emissiveIntensity: 0.06 }));
      ceil.position.y = height; ceil.rotation.x = Math.PI/2; g.add(ceil);
      return g;
    }

    // 三个主题房间：宇宙、生态、海洋（蓝色系变化）
    const roomA = makeRoom(16, 6.5, 16, 0x0f2848); roomA.position.set(-18, 0, 0); rooms.add(roomA);
    const roomB = makeRoom(16, 6.5, 16, 0x0a2c5a); roomB.position.set(   0, 0, 0); rooms.add(roomB);
    const roomC = makeRoom(16, 6.5, 16, 0x0a315f); roomC.position.set( 18, 0, 0); rooms.add(roomC);

    // 连接走道
    const corridor = new THREE.Mesh(new THREE.PlaneGeometry(60, 6), new THREE.MeshStandardMaterial({ color: 0x0d2444, roughness: .9 }));
    corridor.rotation.x = -Math.PI/2; corridor.position.set(0, 0.001, -6);
    rooms.add(corridor);

    // 每个房间中心设置展台和点光
    const pedestals = [];
    function addPedestal(target, color=0x2b68c8) {
      const base = new THREE.Mesh(
        new THREE.CylinderGeometry(0.9, 1.1, 0.6, 32),
        new THREE.MeshStandardMaterial({ color: 0x12345a, metalness: 0.6, roughness: 0.35 })
      );
      base.position.set(0, 0.3, 0); base.castShadow = true; base.receiveShadow = true;
      target.add(base);
      const halo = new THREE.Mesh(new THREE.TorusGeometry(1.05, 0.04, 16, 64), new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.6 }));
      halo.position.y = 0.36; halo.rotation.x = Math.PI/2; halo.castShadow = true; target.add(halo);
      const pl = new THREE.PointLight(0x7fbfff, 1.2, 8, 2.0); pl.position.set(0, 2.2, 0); pl.castShadow = true; target.add(pl);
      pedestals.push(base);
      return base;
    }
    const pedA = addPedestal(roomA);
    const pedB = addPedestal(roomB);
    const pedC = addPedestal(roomC);

    // ============================
    // 加载三个示例 glTF 模型（CDN）
    // ============================
    // 创建 GLTF 加载器，若不可用则稍后采用本地几何体作为回退
    const loader = (typeof THREE.GLTFLoader === 'function') ? new THREE.GLTFLoader() : null;

    function centerAndScale(object, targetScale=1.2) {
      // 将模型居中并缩放到合适尺寸
      const box = new THREE.Box3().setFromObject(object);
      const size = new THREE.Vector3(); box.getSize(size);
      const center = new THREE.Vector3(); box.getCenter(center);
      object.position.sub(center); // 居中
      const maxDim = Math.max(size.x, size.y, size.z);
      const s = targetScale / maxDim; object.scale.setScalar(s);
    }

    function createFallbackMesh(kind='box') {
      let geo; const mat = new THREE.MeshStandardMaterial({ color: 0x5aa3ff, metalness:.6, roughness:.35, emissive:0x0b2b5a, emissiveIntensity:.15 });
      if (kind === 'sphere') geo = new THREE.SphereGeometry(1.1, 32, 16);
      else if (kind === 'knot') geo = new THREE.TorusKnotGeometry(0.8, 0.25, 120, 16);
      else geo = new THREE.BoxGeometry(1.6, 1.1, 1.1);
      const mesh = new THREE.Mesh(geo, mat);
      mesh.castShadow = true; mesh.receiveShadow = true; mesh.position.y = 1.3;
      return mesh;
    }

    function loadModel(url, parent, autoRotateY = 0.3, fallbackKind='box') {
      return new Promise((resolve) => {
        if (!loader) {
          // 回退：构建本地几何体模型
          const m = createFallbackMesh(fallbackKind); m.userData.autoRotateY = autoRotateY; parent.add(m); return resolve(m);
        }
        loader.load(url, (gltf) => {
          const model = gltf.scene;
          centerAndScale(model, 2.2);
          model.position.y = 1.3; // 放在展台上
          model.traverse(o => { if (o.isMesh) { o.castShadow = true; o.receiveShadow = true; } });
          model.userData.autoRotateY = autoRotateY;
          parent.add(model);
          resolve(model);
        }, undefined, (err) => {
          // 加载失败则使用回退几何体，避免报错
          const m = createFallbackMesh(fallbackKind); m.userData.autoRotateY = autoRotateY; parent.add(m); resolve(m);
          console.warn('GLTF 加载失败，使用内置模型回退：', err);
        });
      });
    }

    // 模型来源：threejs.org 官方示例资产
    const assets = [
      // 宇宙主题：DamagedHelmet
      'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf',
      // 生态主题：Flamingo
      'https://threejs.org/examples/models/gltf/Flamingo.glb',
      // 海洋主题：Duck
      'https://threejs.org/examples/models/gltf/Duck/glTF/Duck.gltf'
    ];

    const models = [];
    Promise.all([
      loadModel(assets[0], pedA, 0.2, 'knot'),
      loadModel(assets[1], pedB, 0.4, 'sphere'),
      loadModel(assets[2], pedC, 0.6, 'box'),
    ]).then(ms => models.push(...ms));

    // ============================
    // 第一人称移动（Pointer Lock）
    // ============================
    const controls = new THREE.PointerLockControls(camera, renderer.domElement);
    scene.add(controls.getObject());

    const lockHint = document.getElementById('lockHint');
    // 支持点击提示面板或画布进入
    lockHint.addEventListener('click', () => controls.lock());
    renderer.domElement.addEventListener('click', () => { if (!controls.isLocked) controls.lock(); });
    // 按 Enter 键也可进入
    document.addEventListener('keydown', (e)=>{ if (e.code === 'Enter' && !controls.isLocked) controls.lock(); });
    // 首次进入时，将玩家放在建筑外广场（左前方），面向主立面
    let enteredHall = false;
    controls.addEventListener('lock', () => {
      if (!enteredHall) {
        const obj = controls.getObject();
        // 位置参考：外立面在 z=-12，选择左前方视点以获得图片中的斜向透视
        obj.position.set(-12, 1.7, 18);
        // 朝向建筑中心（0,6,-12）
        camera.lookAt(0, 6, -12);
        enteredHall = true;
      }
      lockHint.style.display = 'none';
    });
    controls.addEventListener('unlock', () => lockHint.style.display = 'flex');

    // 按键状态
    const keys = { w:false, a:false, s:false, d:false, space:false };
    window.addEventListener('keydown', (e)=>{
      if (e.code === 'KeyW') keys.w = true;
      if (e.code === 'KeyA') keys.a = true;
      if (e.code === 'KeyS') keys.s = true;
      if (e.code === 'KeyD') keys.d = true;
      if (e.code === 'Space') keys.space = true;
      if (e.code === 'KeyE') tryToggleInspect();
    });
    window.addEventListener('keyup', (e)=>{
      if (e.code === 'KeyW') keys.w = false;
      if (e.code === 'KeyA') keys.a = false;
      if (e.code === 'KeyS') keys.s = false;
      if (e.code === 'KeyD') keys.d = false;
      if (e.code === 'Space') keys.space = false;
    });

    // 运动学参数
    const velocity = new THREE.Vector3();
    const direction = new THREE.Vector3();
    const GRAVITY = 28;
    let canJump = false;
    let prevTime = performance.now();

    function movePlayer(delta) {
      // 阻尼
      velocity.x -= velocity.x * 10.0 * delta;
      velocity.z -= velocity.z * 10.0 * delta;

      // 重力
      velocity.y -= GRAVITY * delta;

      direction.z = Number(keys.w) - Number(keys.s);
      direction.x = Number(keys.d) - Number(keys.a);
      direction.normalize();

      const speed = 6.0; // 移动速度
      if (keys.w || keys.s) velocity.z -= direction.z * speed * delta * 60;
      if (keys.a || keys.d) velocity.x -= direction.x * speed * delta * 60;

      // 简化的地面碰撞
      const obj = controls.getObject();
      obj.position.x += velocity.x * delta;
      obj.position.z += velocity.z * delta;
      obj.position.y += velocity.y * delta;

      if (obj.position.y < 1.7) {
        velocity.y = 0; obj.position.y = 1.7; canJump = true;
      }
      if (keys.space && canJump) { velocity.y += 10; canJump = false; }

      // 限制在展厅区域内，避免走出世界太远
      obj.position.x = THREE.MathUtils.clamp(obj.position.x, -40, 40);
      obj.position.z = THREE.MathUtils.clamp(obj.position.z, -80, 30);
    }

    // ============================
    // 模型检查模式（近距离交互，鼠标拖动旋转）
    // ============================
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    let inspectMode = false;
    let inspectTarget = null;
    let dragging = false;
    let lastX = 0, lastY = 0;

    const tip = document.getElementById('interactTip');

    function updateInteractTip() {
      // 接近任意展台 2.2 米以内出现提示
      const pos = controls.getObject().position;
      let near = false;
      for (const ped of [pedA, pedB, pedC]) {
        const d = ped.getWorldPosition(new THREE.Vector3()).distanceTo(pos);
        if (d < 2.2) { near = true; break; }
      }
      tip.style.display = (near && !inspectMode) ? 'block' : 'none';
    }

    function tryToggleInspect() {
      const pos = controls.getObject().position;
      if (!inspectMode) {
        // 进入检查模式：选择最近的模型
        let minD = Infinity; let target = null;
        for (const m of models) {
          const d = m.getWorldPosition(new THREE.Vector3()).distanceTo(pos);
          if (d < minD) { minD = d; target = m; }
        }
        if (target && minD < 2.5) {
          inspectMode = true; inspectTarget = target; controls.unlock();
          lockHint.style.display = 'none';
          tip.style.display = 'none';
        }
      } else {
        // 退出检查模式
        inspectMode = false; inspectTarget = null; dragging = false; controls.lock();
      }
    }

    renderer.domElement.addEventListener('mousedown', (e)=>{
      if (!inspectMode) return;
      dragging = true; lastX = e.clientX; lastY = e.clientY;
    });
    renderer.domElement.addEventListener('mouseup', ()=> dragging = false);
    renderer.domElement.addEventListener('mousemove', (e)=>{
      if (!inspectMode || !inspectTarget || !dragging) return;
      const dx = (e.clientX - lastX) * 0.01;
      const dy = (e.clientY - lastY) * 0.01;
      inspectTarget.rotation.y += dx;
      inspectTarget.rotation.x += dy;
      lastX = e.clientX; lastY = e.clientY;
    });

    // ============================
    // 动画循环
    // ============================
    function animate() {
      requestAnimationFrame(animate);
      const time = performance.now();
      const delta = Math.min((time - prevTime) / 1000, 0.05);
      prevTime = time;

      // 非检查模式下，人物自由移动
      if (controls.isLocked) movePlayer(delta);

      updateInteractTip();

      // 模型缓慢自转（非检查模式时）
      for (const m of models) {
        if (!inspectMode && m.userData.autoRotateY) m.rotation.y += m.userData.autoRotateY * delta;
      }

      renderer.render(scene, camera);
    }
    animate();

    // ============================
    // 自适应
    // ============================
    window.addEventListener('resize', ()=>{
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });

    // 初始视角锁定提示：点击屏幕进入
  </script>
</body>
</html>
